1  Prognosemodel retentie na 1 jaar

GVS | B Opleiding tot Verpleegkundige (HBO-V) - voltijd - versie 1.0

Auteur

Theo Bakker, lector Learning Technology & Analytics, De HHs

Publicatiedatum

25 juni 2024

1.1 Methode, data en analyse

1.1.1 Toelichting op de methode

Voor de ontwikkeling van prognosemodellen gebruiken we de aanpak van Tidymodels. Tidymodels is een framework voor het bouwen van een prognosemodel. Hiermee verzekeren we ons van een systematische, herhaalbare en schaalbare aanpak.

1.1.2 Toelichting op de data

De basis voor deze analyse is studiedata van De Haagse Hogeschool (De HHs), verrijkt door het lectoraat LTA. De data bevat informatie over de inschrijvingen van studenten in het eerste jaar van de opleiding:

  1. Demografische kenmerken: geslacht, leeftijd, reistijd en SES totaalscore.
  2. Vooropleidingskenmerken: toelaatgevende vooropleiding, studiekeuzeprofiel, gemiddeld eindcijfer in de vooropleiding en eventuele deelname aan het Navitas programma.
  3. Aanmeldingskenmerken: aansluiting (direct na diploma, tussenjaar, switch), dag van aanmelding, aantal parallelle studies aan De HHs en collegejaar.

1.1.3 Toelichting op de analyse

We toetsen in deze analyse Retentie na 1 jaar, voortaan Retentie genoemd.

Retentie is gedefinieerd als ingeschreven staan in dezelfde opleiding in een aansluitend collegejaar. Een wisseling van opleidingsvorm binnen de opleiding, bijv. van voltijd in jaar 1 naar duaal in jaar 2, geldt ook als retentie.

Uitval is het tegenovergestelde van retentie: niet ingeschreven staan in dezelfde opleiding in een aansluitend collegejaar. Een wisseling van opleidingsvorm binnen de opleiding, bijv. van voltijd in jaar 1 naar duaal in jaar 2, geldt niet als uitval.

1.2 Voorbereidingen

1.2.1 Laad de data

We laden een subset in van historische data specifiek voor:

Opleiding: GVS | B Opleiding tot Verpleegkundige (HBO-V), voltijd, eerstejaars - Retentie na 1 jaar

Toon code
## Laad de data voor de opleiding
dfOpleiding_inschrijvingen_base <- get_lta_studyprogram_enrollments_pin(
    board = "HHs/Inschrijvingen",
    faculty = faculteit,
    studyprogram = opleidingsnaam_huidig,
    studytrack = opleiding,
    studyform = toupper(opleidingsvorm),
    range = "eerstejaars")

## Herschik de levels
Set_Levels(dfOpleiding_inschrijvingen_base)

dfOpleiding_inschrijvingen_base <- dfOpleiding_inschrijvingen_base |>  
  
  ## Maak een eenvoudige succes variabele aan
  Mutate_Retentie(sSucces_model) |>
  
  ## Maak van de succes variabele een factor
  mutate(SUC_Retentie = as.factor(SUC_Retentie)) |> 

  ## Verbijzonder eventueel op basis van het propedeusediploma
  # Filter_Propedeusediploma(sPropedeusediploma) |>

  ## Maak van de Dubbele studie variabele een Ja/Nee variabele
  mutate(INS_Dubbele_studie = ifelse(INS_Aantal_inschrijvingen > 1, "Ja", "Nee")) |>  

  ## Verwijder INS_Aantal_inschrijvingen
  select(-INS_Aantal_inschrijvingen) |> 

  ## Pas voor een aantal variabelen de levels aan
  Mutate_Levels(
  c(
    "VOP_Studiekeuzeprofiel_LTA_afkorting",
    "INS_Aansluiting_LTA",
    "VOP_Toelaatgevende_vooropleiding_soort"
  ),
    list(lLevels_skp, lLevels_vop, lLevels_vop)
  )
  
## B Huidtherapie: Filter op uitsluitend studenten met een rangnummer (selectie)
if(opleiding == "HDT") {
  dfOpleiding_inschrijvingen_base <- dfOpleiding_inschrijvingen_base |> 
    filter(!is.na(RNK_Rangnummer)) 
} 

1.2.2 Selecteer en inspecteer de data

We selecteren eerst de relevante variabelen. We verwijderen daarbij variabelen die maar 1 waarde hebben. We bekijken de variabelen in een samenvatting in relatie tot retentie. Daarnaast bekijken we de kwaliteit van de data op missende waarden.

Toon code
lSelect <- c(
    "INS_Student_UUID_opleiding_vorm",
    "CBS_APCG_tf",
    "DEM_Geslacht",
    "DEM_Leeftijd_1_oktober",
    "GIS_Tijd_fiets_OV",
    "INS_Collegejaar",
    "INS_Dagen_tussen_aanmelding_en_1_september",
    "INS_Dubbele_studie",
    "INS_Aansluiting_LTA",
    "INS_Navitas_tf",
    "SES_Deelscore_arbeid",
    "SES_Deelscore_welvaart",
    "SES_Totaalscore",
    "SUC_Retentie",
    "VOP_Gemiddeld_cijfer_cijferlijst",
    "VOP_Gemiddeld_eindcijfer_VO_van_de_hoogste_vooropleiding_voor_het_HO",
    "VOP_Cijfer_CE1_nederlands",
    "VOP_Cijfer_CE1_engels",
    "VOP_Cijfer_CE_proxy_wiskunde",
    "VOP_Cijfer_CE1_natuurkunde",
    "VOP_Studiekeuzeprofiel_LTA_afkorting",
    "VOP_Toelaatgevende_vooropleiding_soort"
  )

## B Huidtherapie: voeg de variabele RNK_Rangnummer toe
if(opleiding == "HDT") {
  lSelect <- c(lSelect, "RNK_Rangnummer")
}

## Maak een subset
dfOpleiding_inschrijvingen <- dfOpleiding_inschrijvingen_base |>
  
  ## Selecteer de relevante variabelen
  select_at(lSelect) |>
  
  ## Hernoem variabelen voor beter leesbare namen
  rename(
    ID                    = INS_Student_UUID_opleiding_vorm,
    Geslacht              = DEM_Geslacht,
    Leeftijd              = DEM_Leeftijd_1_oktober,
    Reistijd              = GIS_Tijd_fiets_OV,
    Dubbele_studie        = INS_Dubbele_studie,
    Collegejaar           = INS_Collegejaar,
    Aanmelding            = INS_Dagen_tussen_aanmelding_en_1_september,
    Aansluiting           = INS_Aansluiting_LTA,
    Navitas               = INS_Navitas_tf,
    APCG                  = CBS_APCG_tf,
    SES_Arbeid            = SES_Deelscore_arbeid,
    SES_Welvaart          = SES_Deelscore_welvaart,
    SES_Totaal            = SES_Totaalscore,          
    Retentie              = SUC_Retentie,
    Cijfer_SE_VO          = VOP_Gemiddeld_cijfer_cijferlijst,
    Cijfer_CE_VO          = VOP_Gemiddeld_eindcijfer_VO_van_de_hoogste_vooropleiding_voor_het_HO,
    Cijfer_CE_Nederlands  = VOP_Cijfer_CE1_nederlands,
    Cijfer_CE_Engels      = VOP_Cijfer_CE1_engels,
    Cijfer_CE_Wiskunde    = VOP_Cijfer_CE_proxy_wiskunde,
    Cijfer_CE_Natuurkunde = VOP_Cijfer_CE1_natuurkunde,
    Studiekeuzeprofiel    = VOP_Studiekeuzeprofiel_LTA_afkorting,
    Vooropleiding         = VOP_Toelaatgevende_vooropleiding_soort
  ) |> 
  
  ## Pas CBS_APCG_tf aan naar factor
  mutate(APCG = case_when(APCG == TRUE ~ "Ja",
                          APCG == FALSE ~ "Nee",
                          .default = "Onbekend")) |>

  ## Geef aan waar missende cijfers in het VO zijn
  Mutate_Cijfers_VO() |>
  
  ## Verwijder variabelen, waarbij er maar 1 waarde is
  select(where(~ n_distinct(.) > 1)) |>
  
  arrange(Collegejaar, ID)

## B Huidtherapie: hernoem de variabele RNK_Rangnummer
if(opleiding == "HDT") {
  dfOpleiding_inschrijvingen <- dfOpleiding_inschrijvingen |> 
    rename(Rangnummer = RNK_Rangnummer)
} 

dfOpleiding_inschrijvingen <- dfOpleiding_inschrijvingen |> 
 ltabase::sort_distinct()

## Verwijder de basis dataset
rm(dfOpleiding_inschrijvingen_base)
Studentkenmerken versus Retentie
Variabele Retentie p-value2 Totaal, N = 19761
Ja, N=1194 (60%)1 Nee, N=782 (40%)1
Aanmelding 152,72 (72,70) 133,49 (73,76) <0,001*** 145,11 (73,71)
Aansluiting

0,051
    2e Studie 14 (67%) 7 (33%)
21 (100%)
    Direct 622 (62%) 386 (38%)
1.008 (100%)
    Na CD 23 (82%) 5 (18%)
28 (100%)
    Overig 1 (33%) 2 (67%)
3 (100%)
    Switch extern 358 (60%) 242 (40%)
600 (100%)
    Switch intern 38 (50%) 38 (50%)
76 (100%)
    Tussenjaar 138 (58%) 102 (43%)
240 (100%)
APCG

<0,001***
    Ja 389 (55%) 317 (45%)
706 (100%)
    Nee 701 (65%) 379 (35%)
1.080 (100%)
    Onbekend 104 (55%) 86 (45%)
190 (100%)
Cijfer_CE_Engels 6,52 (1,23) 6,87 (1,11) <0,001*** 6,64 (1,20)
Cijfer_CE_Engels_missing

<0,001***
    Ja 579 (56%) 457 (44%)
1.036 (100%)
    Nee 615 (65%) 325 (35%)
940 (100%)
Cijfer_CE_Natuurkunde 6,18 (0,96) 6,15 (1,06) 0,88 6,17 (0,99)
Cijfer_CE_Natuurkunde_missing

0,007**
    Ja 989 (59%) 683 (41%)
1.672 (100%)
    Nee 205 (67%) 99 (33%)
304 (100%)
Cijfer_CE_Nederlands 6,15 (0,87) 6,14 (0,92) 0,73 6,15 (0,89)
Cijfer_CE_Nederlands_missing

<0,001***
    Ja 579 (56%) 456 (44%)
1.035 (100%)
    Nee 615 (65%) 326 (35%)
941 (100%)
Cijfer_CE_VO 6,52 (0,42) 6,42 (0,40) <0,001*** 6,49 (0,41)
Cijfer_CE_VO_missing

<0,001***
    Ja 492 (53%) 428 (47%)
920 (100%)
    Nee 702 (66%) 354 (34%)
1.056 (100%)
Cijfer_CE_Wiskunde 6,49 (1,19) 6,49 (1,14) 0,90 6,49 (1,17)
Cijfer_CE_Wiskunde_missing

<0,001***
    Ja 625 (56%) 488 (44%)
1.113 (100%)
    Nee 569 (66%) 294 (34%)
863 (100%)
Cijfer_SE_VO 6,52 (0,42) 6,43 (0,40) <0,001*** 6,49 (0,41)
Cijfer_SE_VO_missing

<0,001***
    Ja 536 (55%) 446 (45%)
982 (100%)
    Nee 658 (66%) 336 (34%)
994 (100%)
Dubbele_studie

0,060
    Ja 9 (41%) 13 (59%)
22 (100%)
    Nee 1.185 (61%) 769 (39%)
1.954 (100%)
Geslacht

<0,001***
    M 158 (46%) 183 (54%)
341 (100%)
    V 1.036 (63%) 599 (37%)
1.635 (100%)
Leeftijd 20,02 (3,24) 20,64 (3,41) <0,001*** 20,26 (3,32)
Reistijd 32,49 (17,03) 32,44 (18,01) 0,50 32,47 (17,42)
SES_Arbeid -0,01 (0,10) -0,03 (0,10) <0,001*** -0,02 (0,10)
SES_Totaal -0,07 (0,33) -0,13 (0,35) <0,001*** -0,09 (0,34)
SES_Welvaart -0,03 (0,15) -0,06 (0,16) <0,001*** -0,04 (0,16)
Studiekeuzeprofiel

<0,001***
    EM 83 (58%) 59 (42%)
142 (100%)
    CM 105 (64%) 60 (36%)
165 (100%)
    EM&CM 87 (73%) 32 (27%)
119 (100%)
    NT 4 (25%) 12 (75%)
16 (100%)
    NG 349 (65%) 192 (35%)
541 (100%)
    NT&NG 134 (69%) 60 (31%)
194 (100%)
    OS 3 (100%) 0 (0%)
3 (100%)
    CERT 2 (100%) 0 (0%)
2 (100%)
    ALG 3 (50%) 3 (50%)
6 (100%)
    BI 0 (0%) 1 (100%)
1 (100%)
    EA 42 (41%) 61 (59%)
103 (100%)
    HO 16 (64%) 9 (36%)
25 (100%)
    HB 6 (50%) 6 (50%)
12 (100%)
    ICT 3 (100%) 0 (0%)
3 (100%)
    MedV 12 (60%) 8 (40%)
20 (100%)
    TP 4 (80%) 1 (20%)
5 (100%)
    TR 10 (63%) 6 (38%)
16 (100%)
    TSL 2 (50%) 2 (50%)
4 (100%)
    UV 6 (50%) 6 (50%)
12 (100%)
    VS 4 (67%) 2 (33%)
6 (100%)
    VNL 7 (54%) 6 (46%)
13 (100%)
    ZW 169 (49%) 176 (51%)
345 (100%)
Vooropleiding

<0,001***
    MBO 284 (50%) 289 (50%)
573 (100%)
    HAVO 697 (65%) 379 (35%)
1.076 (100%)
    VWO 70 (66%) 36 (34%)
106 (100%)
    BD 60 (55%) 50 (45%)
110 (100%)
    HO 56 (73%) 21 (27%)
77 (100%)
    CD 27 (79%) 7 (21%)
34 (100%)
1 Mean (SD); n (%)
2 *p<0.05; **p<0.01; ***p<0.001
Toon code
## Laad dlookr
suppressMessages(library(dlookr))

## Toon een samenvatting van de data, gesorteerd op missende waarden
diagnose(dfOpleiding_inschrijvingen) |> 
  mutate(missing_percent = round(missing_percent, 2),
         unique_rate = round(missing_percent, 2)) |>
  arrange(desc(missing_percent)) |>
  knitr::kable(caption = "Kwaliteit van de data voor bewerkingen (gesorteerd op missende waarden)",
               col.names = c("Variabelen",
                           "Type",
                           "# Missende waarden",
                           "% Missende waarden",
                           "# Unieke waarden",
                           "Ratio unieke waarden"))
Kwaliteit van de data voor bewerkingen (gesorteerd op missende waarden)
Variabelen Type # Missende waarden % Missende waarden # Unieke waarden Ratio unieke waarden
Cijfer_CE_Natuurkunde numeric 1672 84.62 49 84.62
Cijfer_CE_Wiskunde numeric 1113 56.33 62 56.33
Cijfer_CE_Engels numeric 1036 52.43 67 52.43
Cijfer_CE_Nederlands numeric 1035 52.38 54 52.38
Cijfer_SE_VO numeric 982 49.70 27 49.70
Cijfer_CE_VO numeric 920 46.56 27 46.56
Studiekeuzeprofiel factor 223 11.29 23 11.29
SES_Arbeid numeric 190 9.62 312 9.62
SES_Totaal numeric 190 9.62 626 9.62
SES_Welvaart numeric 190 9.62 437 9.62
Reistijd numeric 34 1.72 396 1.72
Aanmelding numeric 0 0.00 322 0.00
Aansluiting factor 0 0.00 7 0.00
APCG character 0 0.00 3 0.00
Cijfer_CE_Engels_missing character 0 0.00 2 0.00
Cijfer_CE_Natuurkunde_missing character 0 0.00 2 0.00
Cijfer_CE_Nederlands_missing character 0 0.00 2 0.00
Cijfer_CE_VO_missing character 0 0.00 2 0.00
Cijfer_CE_Wiskunde_missing character 0 0.00 2 0.00
Cijfer_SE_VO_missing character 0 0.00 2 0.00
Collegejaar numeric 0 0.00 11 0.00
Dubbele_studie character 0 0.00 2 0.00
Geslacht character 0 0.00 2 0.00
ID character 0 0.00 1976 0.00
Leeftijd integer 0 0.00 24 0.00
Retentie factor 0 0.00 2 0.00
Vooropleiding factor 0 0.00 6 0.00
Toon code
## Verwijder dlookr
detach("package:dlookr", unload = TRUE)

1.2.3 Bewerk de data

  • Uit de eerste diagnose blijkt dat niet alle variabelen goed genoeg zijn voor het bouwen van een prognosemodel: er zijn missende waarden en niet alle veldtypes zijn geschikt. We passen de variabelen aan zodat we in het model er goed mee kunnen werken.
  • Prognosemodellen kunnen niet omgaan met missende waarden. Om bias te voorkomen verwijderen we geen rijen met missende waarden, maar vullen die op (imputatie). We bewerken de data zo dat alle missende waarden worden opgevuld: bij numerieke waarden met het gemiddelde en bij categorische variabelen met ‘Onbekend’.
  • We passen sommige variabelen aan, zodat ze in het model gebruikt kunnen worden: tekstvelden zetten we om naar factor (een categorische variabele); logische variabelen (Ja/Nee) zetten we om naar een numerieke variabele (1/0).
  • De uitkomstvariabele, Retentie, leiden we af van de variabele SUC_Uitval_aantal_jaar_LTA. Als de waarde daar 1 is, is de student na 1 jaar uitgevallen, 2 na 2 jaar, etc. Zolang de waarde daar 0 is, is de student niet uitgevallen.
  • Een fictief studentnummer (INS_Student_UUID_opleiding_vorm) gebruiken we, zodat we - als er afwijkende resultaten zijn - de dataset gericht kunnen onderzoeken indien nodig.
Toon code
## Bewerk de data
dfOpleiding_inschrijvingen <- dfOpleiding_inschrijvingen |> 
  
  ## Imputeer alle numerieke variabelen met de mean
  mutate(across(where(is.numeric), ~ ifelse(
    is.na(.x),
    mean(.x, na.rm = T),
    .x
  )) ) |>
  
  ## Zet character variabelen om naar factor
  mutate(across(where(is.character), as.factor)) |> 
  
  ## Zet logische variabelen om naar 0 of 1
  mutate(across(where(is.logical), as.integer)) |>
  
  ## Vul in factoren missende waarden op met "Onbekend"
  mutate(across(where(is.factor), ~ suppressWarnings(
    fct_explicit_na(.x, na_level = "Onbekend")
  ))) |> 
  
  ## Herschik de kolommen, zodat Retentie vooraan staat
  select(Retentie, everything()) 

## Bekijk de data
## glimpse(dfOpleiding_inschrijvingen) 

## Laad dlookr
suppressMessages(library(dlookr))

## Maak een diagnose van de data
diagnose(dfOpleiding_inschrijvingen) |> 
  mutate(missing_percent = round(missing_percent, 2),
         unique_rate = round(unique_rate, 2)) |>
  knitr::kable(caption = "Kwaliteit van de data na bewerkingen",
               col.names = c("Variabelen",
                           "Type",
                           "# Missende waarden",
                           "% Missende waarden",
                           "# Unieke waarden",
                           "Ratio unieke waarden"))
Kwaliteit van de data na bewerkingen
Variabelen Type # Missende waarden % Missende waarden # Unieke waarden Ratio unieke waarden
Retentie factor 0 0 2 0.00
Aanmelding numeric 0 0 322 0.16
Aansluiting factor 0 0 7 0.00
APCG factor 0 0 3 0.00
Cijfer_CE_Engels numeric 0 0 67 0.03
Cijfer_CE_Engels_missing factor 0 0 2 0.00
Cijfer_CE_Natuurkunde numeric 0 0 49 0.02
Cijfer_CE_Natuurkunde_missing factor 0 0 2 0.00
Cijfer_CE_Nederlands numeric 0 0 54 0.03
Cijfer_CE_Nederlands_missing factor 0 0 2 0.00
Cijfer_CE_VO numeric 0 0 27 0.01
Cijfer_CE_VO_missing factor 0 0 2 0.00
Cijfer_CE_Wiskunde numeric 0 0 62 0.03
Cijfer_CE_Wiskunde_missing factor 0 0 2 0.00
Cijfer_SE_VO numeric 0 0 27 0.01
Cijfer_SE_VO_missing factor 0 0 2 0.00
Collegejaar numeric 0 0 11 0.01
Dubbele_studie factor 0 0 2 0.00
Geslacht factor 0 0 2 0.00
ID factor 0 0 1976 1.00
Leeftijd integer 0 0 24 0.01
Reistijd numeric 0 0 396 0.20
SES_Arbeid numeric 0 0 312 0.16
SES_Totaal numeric 0 0 626 0.32
SES_Welvaart numeric 0 0 437 0.22
Studiekeuzeprofiel factor 0 0 23 0.01
Vooropleiding factor 0 0 6 0.00
Toon code
detach("package:dlookr", unload = TRUE)

1.2.4 Bekijk de onderlinge correlaties

Het is verstandig om voorafgaand aan het bouwen van een model te kijken naar de onderlinge correlaties tussen numerieke variabelen. Dit geeft inzicht in de data en kan helpen bij het maken van keuzes voor het model of de duiding van de uitkomsten.

Toon code
## Maak een plot van de onderlinge correlaties in numerieke variabelen
dfOpleiding_inschrijvingen |> 
  select(-Collegejaar) |>
  select(where(is.numeric)) |> 
  cor() |> 
  corrplot::corrplot(
    order = 'hclust', 
    addrect = 4,
    method = "number",  
    tl.cex = 0.8,       
    tl.col = "black",
    diag = FALSE)

1.2.5 Bouw de trainingset, validatieset en testset

  • De data is nu geschikt om een prognosemodel mee te bouwen.
  • Om het model te bouwen, testen en valideren, splitsen we de data in drie delen van 60%, 20% en 20%. We doen dit op zo’n manier, dat elk deel ongeveer een gelijk aantal studenten bevat dat doorstudeert (dus niet uitvalt).
  • We trainen het model op basis van 60% en valideren de modellen tijdens het trainen op de overige 20% (de validatieset).
  • De verdeling van de training- en validatieset muteren we 10x (10 folds) om te voorkomen dat het model te veel leert van de trainingset en daardoor slecht presteert op de validatieset.
  • Als het model klaar is, testen we het op de 20% studenten uit de testset. De testset blijft dus de gehele tijd ongemoeid, zodat we overfitting - een te goed model op bekende data, maar slechte presetaties (performance) op onbekende data - voorkomen.
  • Een willekeurig, maar vaststaand seed-getal voorkomt dat we bij elke run van het model c.q. deze code een net iets andere uitkomst krijgen.

Toon code
set.seed(0821)

## Splits de data in 3 delen: 60%, 20% en 20%
splits      <- initial_validation_split(dfOpleiding_inschrijvingen,
                                        strata = Retentie,
                                        prop = c(0.6, 0.2))

## Maak drie sets: een trainingset, een testset en een validatieset
dfRetentie_train      <- training(splits)
dfRetentie_test       <- testing(splits)
dfRetentie_validation <- validation_set(splits)

## Maak een resample set op basis van 10 folds (default)
dfRetentie_resamples  <- vfold_cv(dfRetentie_train, strata = Retentie)
Verhouding training- en testset
Naam Retentie Aantal Proportie
Trainingset FALSE 469 39.6%
Trainingset TRUE 716 60.4%
Testset FALSE 157 39.6%
Testset TRUE 239 60.4%

1.3 Model I: Logistische Regressie

  • Het eerste model is een logistische regressie met penalized likelihood; we gebruiken de glmnet engine voor het bouwen van het model. Penalized likelihood is een techniek die helpt bij het voorkomen van overfitting. Glmnet is een populair package voor het bouwen van logistische regressiemodellen.
  • We gebruiken de Area under the ROC Curve (AUC/ROC) als performance metric.

1.3.1 Maak het model

Eerst bouwen we het model.

## Bouw het model: logistische regressie
lr_mod <- 
  logistic_reg(penalty = tune(), mixture = 1) |> 
  set_engine("glmnet")

1.3.2 Maak de recipe

Vervolgens zetten we meerdere stappen in een ‘recipe’:

  • We definiëren de student-ID als ID variabele. Daarmee krijgt deze variabele de rol van uniek rij-kenmerk.
  • We verwijderen vervolgens de oorspronkelijke student-ID en het collegejaar uit de data, omdat deze verder niet gebruikt moeten worden in het model.
  • We converteren factoren naar dummy variabelen.
  • We verwijderen variabelen die geen waarde toevoegen: variabelen met enkel nullen.
  • We transformeren numerieke variabelen om ze met elkaar te kunnen vergelijken door ze te centreren en schalen.
  • Sterk gecorreleerde waarden verwijderen we nu niet, omdat we later in de analyse de eventuele samenhang met andere variabelen in een prognosemodel nog willen kunnen visualiseren.
## Bouw de recipe: logistische regressie
lr_recipe <- 
  recipe(Retentie ~ ., data = dfRetentie_train) |>  
  update_role(ID, new_role = "ID") |>           ## Zet de student ID als ID variabele
  step_rm(ID, Collegejaar) |>                   ## Verwijder ID en collegejaar uit het model
  step_dummy(all_nominal_predictors()) |>       ## Maak dummy variabelen van categorische variabelen
  step_zv(all_predictors()) |>                  ## Verwijder zero values
  step_normalize(all_numeric_predictors())      ## Centreer en schaal numerieke variabelen

## Toon de recipe
tidy(lr_recipe) |> 
  knitr::kable(col.names = c("Nummer", 
                             "Operatie", 
                             "Type",
                             "Getraind",
                             "Sla over",
                             "ID"))
Nummer Operatie Type Getraind Sla over ID
1 step rm FALSE FALSE rm_N9m1M
2 step dummy FALSE FALSE dummy_cxZIQ
3 step zv FALSE FALSE zv_bTXKn
4 step normalize FALSE FALSE normalize_xFD8n

De variabelen die nu nog resteren zijn:

Resterende variabelen na bewerkingen
Aanmelding APCG_Nee Studiekeuzeprofiel_HO
Cijfer_CE_Engels APCG_Onbekend Studiekeuzeprofiel_HB
Cijfer_CE_Natuurkunde Cijfer_CE_Engels_missing_Nee Studiekeuzeprofiel_ICT
Cijfer_CE_Nederlands Cijfer_CE_Natuurkunde_missing_Nee Studiekeuzeprofiel_MedV
Cijfer_CE_VO Cijfer_CE_Nederlands_missing_Nee Studiekeuzeprofiel_TP
Cijfer_CE_Wiskunde Cijfer_CE_VO_missing_Nee Studiekeuzeprofiel_TR
Cijfer_SE_VO Cijfer_CE_Wiskunde_missing_Nee Studiekeuzeprofiel_TSL
Leeftijd Cijfer_SE_VO_missing_Nee Studiekeuzeprofiel_UV
Reistijd Dubbele_studie_Nee Studiekeuzeprofiel_VS
SES_Arbeid Geslacht_V Studiekeuzeprofiel_VNL
SES_Totaal Studiekeuzeprofiel_CM Studiekeuzeprofiel_ZW
SES_Welvaart Studiekeuzeprofiel_EM.CM Studiekeuzeprofiel_Onbekend
Retentie Studiekeuzeprofiel_NT Vooropleiding_HAVO
Aansluiting_Direct Studiekeuzeprofiel_NG Vooropleiding_VWO
Aansluiting_Na.CD Studiekeuzeprofiel_NT.NG Vooropleiding_BD
Aansluiting_Overig Studiekeuzeprofiel_OS Vooropleiding_HO
Aansluiting_Switch.extern Studiekeuzeprofiel_CERT Vooropleiding_CD
Aansluiting_Switch.intern Studiekeuzeprofiel_ALG
Aansluiting_Tussenjaar Studiekeuzeprofiel_EA

1.3.3 Maak de workflow

Voor de uitvoering bouwen we een nieuwe workflow. Daaraan voegen we het model en de bewerkingen in de recipe toe.

## Maak de workflow: logistische regressie
lr_workflow <- 
  workflow() |>         ## Maak een workflow
  add_model(lr_mod) |>  ## Voeg het model toe
  add_recipe(lr_recipe) ## Voeg de recipe toe

## Toon de workflow
lr_workflow
══ Workflow ════════════════════════════════════════════════════════════════════
Preprocessor: Recipe
Model: logistic_reg()

── Preprocessor ────────────────────────────────────────────────────────────────
4 Recipe Steps

• step_rm()
• step_dummy()
• step_zv()
• step_normalize()

── Model ───────────────────────────────────────────────────────────────────────
Logistic Regression Model Specification (classification)

Main Arguments:
  penalty = tune()
  mixture = 1

Computational engine: glmnet 

1.3.4 Tune en train het model

Het model moet getuned worden. Dit houdt in dat we de beste parameters voor het model moeten vinden. We maken een grid met verschillende penalty waarden. Daarmee kunnen we vervolgens het beste model selecteren met de hoogste ROC/AUC. We plotten de resultaten van de tuning, zodat we hieruit het beste model kunnen kiezen.

## Maak een grid: logistische regressie
lr_reg_grid <- tibble(penalty = 10 ^ seq(-4, -1, length.out = 30))

## Train en tune het model: logistische regressie
lr_res <- 
  lr_workflow |> 
  tune_grid(dfRetentie_validation,
            grid = lr_reg_grid,
            control = control_grid(save_pred = TRUE),
            metrics = metric_set(roc_auc))
Toon code
## Plot de resultaten + een rode verticale lijn voor de max AUC
lr_plot <- 
  lr_res |> 
  collect_metrics() |> 
  ggplot(aes(x = penalty, y = mean)) + 
  geom_point() + 
  geom_line() + 
  
  ## Maak de schaal van de x-as logaritmisch
  scale_x_log10(labels = scales::label_number()) +
    theme(
      axis.title.x = element_text(margin = margin(t = 20))
    ) +
  
  # Bepaal de titel, ondertitel en caption
  labs(
    caption = sCaption,
    x = "Area under the ROC Curve",
    y = "Penalty"
  )
  
  ## Voeg LTA elementen toe
  lr_plot <- Add_LTA_Theme_Elements(lr_plot, title_subtitle = FALSE)
  
# Zoek de penalty waarde met de max AUC
max_auc_penalty <- lr_res |> 
  collect_metrics() |> 
  filter(mean == max(mean)) |> 
  pull(penalty)

# Voeg de rode verticale lijn toe aan lr_plot
lr_plot_plus <- lr_plot + 
  geom_vline(xintercept = max_auc_penalty, color = "red")

# Vind een mean voor de max AUC die hoger is
max_auc_mean <- lr_res |> 
  collect_metrics() |> 
  filter(mean == max(mean)) |> 
  pull(penalty)

## Print de definitieve plot
lr_plot_plus

1.3.5 Kies het beste model

We evalueren modellen met een zo hoog mogelijke Area under the ROC Curve (AUC/ROC) en een zo laag mogelijke penalty. Zo kunnen we uit de resultaten het beste model kiezen. Tot slot maken we een ROC curve om de prestaties van het model te visualiseren.

## Toon het beste model
top_models <-
  lr_res |> 
  show_best(metric = "roc_auc", n = 10) |> 
  mutate(mean = round(mean, 6)) |>
  arrange(penalty) 

top_models|> 
  knitr::kable(col.names = c("Penalty", 
                             "Metriek", 
                             "Estimator",
                             "Gemiddelde",
                             "Aantal",
                             "SE",
                             "Configuratie"))
Penalty Metriek Estimator Gemiddelde Aantal SE Configuratie
0.0028072 roc_auc binary 0.643279 1 NA Preprocessor1_Model15
0.0035622 roc_auc binary 0.645693 1 NA Preprocessor1_Model16
0.0045204 roc_auc binary 0.647490 1 NA Preprocessor1_Model17
0.0057362 roc_auc binary 0.651352 1 NA Preprocessor1_Model18
0.0072790 roc_auc binary 0.654758 1 NA Preprocessor1_Model19
0.0092367 roc_auc binary 0.655321 1 NA Preprocessor1_Model20
0.0117210 roc_auc binary 0.654570 1 NA Preprocessor1_Model21
0.0148735 roc_auc binary 0.654812 1 NA Preprocessor1_Model22
0.0188739 roc_auc binary 0.653256 1 NA Preprocessor1_Model23
0.0239503 roc_auc binary 0.647557 1 NA Preprocessor1_Model24
## Selecteer het beste model: logistische regressie
lr_best <- 
  lr_res |> 
  collect_metrics() |> 
  filter(mean == max(mean)) |>
  slice(1) 

lr_best|> 
  mutate(mean = round(mean, 6)) |>
  knitr::kable(col.names = c("Penalty", 
                             "Metriek", 
                             "Estimator",
                             "Gemiddelde",
                             "Aantal",
                             "SE",
                             "Configuratie"))
Penalty Metriek Estimator Gemiddelde Aantal SE Configuratie
0.0092367 roc_auc binary 0.655321 1 NA Preprocessor1_Model20
## Verzamel de predicties en evalueer het model (AUC/ROC): logistische regressie
lr_auc <- 
  lr_res |> 
  collect_predictions(parameters = lr_best) |> 
  roc_curve(Retentie, .pred_FALSE) |> 
  mutate(model = "Logistisch Regressie")

## Plot de ROC curve
Get_ROC_Plot(lr_auc, position = 1)

## Bepaal de AUC van het beste model
lr_auc_highest   <-
  lr_res |>
  collect_predictions(parameters = lr_best) |> 
  roc_auc(Retentie, .pred_FALSE)

## Voeg de naam van het model en de AUC toe dfModel_results
dfModel_results <- 
  dfModel_results |>
  add_row(model = "Logistic Regression", auc = lr_auc_highest$.estimate)

1.4 Model II: Tree-based ensemble

  • Het tweede model is een random forest: een ensemble van beslisbomen (decision trees). Het is een krachtig model dat goed om kan gaan met complexe data en veel variabelen.
  • We gebruiken de ranger engine voor het bouwen van het model.

1.4.1 Bepaal het aantal PC-cores

Omdat een random forest model veel berekeningen vereist, willen we daarvoor alle computerkracht gebruiken die beschikbaar is. Het aantal CPU’s (cores) van de computer bepaalt hoe snel het model getraind kan worden. Deze informatie gebruiken we bij het bouwen van het model.

Toon code
## Bepaal het aantal cores
cores <- parallel::detectCores()

1.4.2 Maak het model

We bouwen eerst het model. We gebruiken de rand_forest functie om het model te bouwen. We tunen de mtry en min_n parameters. De mtry parameter bepaalt het aantal variabelen dat per boom wordt gebruikt. De min_n parameter bepaalt het minimum aantal observaties dat in een blad van de boom moet zitten. De functie tune() is hier nog een placeholder om de beste waarden voor deze parameters - die we later bepalen - daar in te stellen. We gebruiken 1.000 bomen c.q. versies van het model.

## Bouw het model: random forest

rf_mod <- 
  rand_forest(mtry = tune(), min_n = tune(), trees = 1000) |> 
  set_engine("ranger", num.threads = cores) |> 
  set_mode("classification")

1.4.3 Maak de recipe

We maken een recipe voor het random forest model. We verwijderen de student ID en het collegejaar uit de data, omdat deze niet moet worden gebruikt in het model. Overige stappen zijn bij een random forest minder relevant in tegenstelling tot een regressiemodel.

## Maak de recipe: random forest
rf_recipe <- 
  recipe(Retentie ~ ., data = dfRetentie_train) |> 
  step_rm(ID, Collegejaar)                      ## Verwijder ID en Collegejaar uit het model
  
## Toon de recipe
tidy(rf_recipe) |> 
  knitr::kable(col.names = c("Nummer", 
                             "Operatie", 
                             "Type",
                             "Getraind",
                             "Sla over",
                             "ID"))
Nummer Operatie Type Getraind Sla over ID
1 step rm FALSE FALSE rm_XDrxx

1.4.4 Maak de workflow

We voegen het model en de recipe toe aan de workflow voor dit model.

## Maak de workflow: random forest
rf_workflow <- 
  workflow() |> 
  add_model(rf_mod) |> 
  add_recipe(rf_recipe)

## Toon de workflow
rf_workflow
══ Workflow ════════════════════════════════════════════════════════════════════
Preprocessor: Recipe
Model: rand_forest()

── Preprocessor ────────────────────────────────────────────────────────────────
1 Recipe Step

• step_rm()

── Model ───────────────────────────────────────────────────────────────────────
Random Forest Model Specification (classification)

Main Arguments:
  mtry = tune()
  trees = 1000
  min_n = tune()

Engine-Specific Arguments:
  num.threads = cores

Computational engine: ranger 

1.4.5 Tune en train het model

We trainen en tunen het model in de workflow. We maken een grid met verschillende waarden voor de parameters mtry en min_n. We gebruiken de Area under the ROC Curve (AUC/ROC) als performance metric. Met de resultaten van de tuning kiezen we het beste model.

## Toon de parameters die getuned kunnen worden
rf_mod
Random Forest Model Specification (classification)

Main Arguments:
  mtry = tune()
  trees = 1000
  min_n = tune()

Engine-Specific Arguments:
  num.threads = cores

Computational engine: ranger 
## Extraheer de parameters die getuned worden
extract_parameter_set_dials(rf_mod)
Collection of 2 parameters for tuning

 identifier  type    object
       mtry  mtry nparam[?]
      min_n min_n nparam[+]

Model parameters needing finalization:
   # Randomly Selected Predictors ('mtry')

See `?dials::finalize` or `?dials::update.parameters` for more information.
## Bepaal de seed
set.seed(2904)

## Bouw het grid: random forest
rf_res <- 
  rf_workflow |> 
  tune_grid(dfRetentie_validation,
            grid = 25,
            control = control_grid(save_pred = TRUE),
            metrics = metric_set(roc_auc))
i Creating pre-processing data to finalize unknown parameter: mtry

1.4.6 Kies het beste model

We evalueren de beste modellen en maken een ROC curve om de performance van het model te visualiseren. Vervolgens vergelijken we de prestaties van de modellen en kiezen we het beste model.

## Toon de beste modellen
rf_res |> 
  show_best(metric = "roc_auc", n = 15) |> 
  mutate(mean = round(mean, 6)) |>
  knitr::kable(col.names = c("Mtry", 
                             "Min. aantal", 
                             "Metriek",
                             "Estimator",
                             "Gemiddelde",
                             "Aantal",
                             "SE",
                             "Configuratie"))
Mtry Min. aantal Metriek Estimator Gemiddelde Aantal SE Configuratie
2 6 roc_auc binary 0.642742 1 NA Preprocessor1_Model22
2 32 roc_auc binary 0.641482 1 NA Preprocessor1_Model03
3 8 roc_auc binary 0.635930 1 NA Preprocessor1_Model08
4 26 roc_auc binary 0.633113 1 NA Preprocessor1_Model23
7 35 roc_auc binary 0.632845 1 NA Preprocessor1_Model14
5 36 roc_auc binary 0.631397 1 NA Preprocessor1_Model09
8 31 roc_auc binary 0.629788 1 NA Preprocessor1_Model24
22 38 roc_auc binary 0.629680 1 NA Preprocessor1_Model01
11 21 roc_auc binary 0.628178 1 NA Preprocessor1_Model15
6 23 roc_auc binary 0.628017 1 NA Preprocessor1_Model17
15 8 roc_auc binary 0.627427 1 NA Preprocessor1_Model06
18 40 roc_auc binary 0.627427 1 NA Preprocessor1_Model16
16 34 roc_auc binary 0.626730 1 NA Preprocessor1_Model07
13 25 roc_auc binary 0.626113 1 NA Preprocessor1_Model25
19 3 roc_auc binary 0.625858 1 NA Preprocessor1_Model21
## Plot de resultaten
autoplot <- autoplot(rf_res) +
  theme_minimal() +
  labs(
    y = "roc/auc",
    caption = sCaption
  )
  
  ## Voeg LTA elementen toe
  autoplot <- Add_LTA_Theme_Elements(autoplot, title_subtitle = FALSE)
  
  print(autoplot)

## Selecteer het beste model
rf_best <- 
  rf_res |> 
  select_best(metric = "roc_auc")

rf_best|> 
  knitr::kable(col.names = c("Mtry", 
                             "Min. aantal", 
                             "Configuratie"))
Mtry Min. aantal Configuratie
2 6 Preprocessor1_Model22
Toon code
## Verzamel de predicties
rf_res |> 
  collect_predictions() |> 
  head(10) |>
  mutate(.pred_FALSE = scales::percent(.pred_FALSE, accuracy = 0.1),
         .pred_TRUE = scales::percent(.pred_TRUE, accuracy = 0.1)) |>
  knitr::kable(col.names = c("% Voorsp. FALSE", 
                             "% Voorsp. TRUE", 
                             "ID",
                             "Rij",
                             "Mtry", 
                             "Min. aantal", 
                             "Retentie",
                             "Configuratie"))
% Voorsp. FALSE % Voorsp. TRUE ID Rij Mtry Min. aantal Retentie Configuratie
28.3% 71.7% validation 1186 22 38 FALSE Preprocessor1_Model01
45.5% 54.5% validation 1187 22 38 FALSE Preprocessor1_Model01
30.0% 70.0% validation 1188 22 38 TRUE Preprocessor1_Model01
29.5% 70.5% validation 1189 22 38 TRUE Preprocessor1_Model01
66.3% 33.7% validation 1190 22 38 FALSE Preprocessor1_Model01
59.8% 40.2% validation 1191 22 38 FALSE Preprocessor1_Model01
56.4% 43.6% validation 1192 22 38 FALSE Preprocessor1_Model01
13.4% 86.6% validation 1193 22 38 TRUE Preprocessor1_Model01
36.1% 63.9% validation 1194 22 38 FALSE Preprocessor1_Model01
26.0% 74.0% validation 1195 22 38 TRUE Preprocessor1_Model01
Toon code
## Bepaal de AUC/ROC curve
rf_auc <- 
  rf_res |> 
  collect_predictions(parameters = rf_best) |> 
  roc_curve(Retentie, .pred_FALSE) |> 
  mutate(model = "Random Forest")

## Plot de ROC curve
Get_ROC_Plot(rf_auc, position = 2)

Toon code
## Bepaal de AUC van het beste model
rf_auc_highest   <-
  rf_res |>
  collect_predictions(parameters = rf_best) |> 
  roc_auc(Retentie, .pred_FALSE)

## Voeg de naam van het model en de AUC toe dfModel_results
dfModel_results <- 
  dfModel_results |>
  add_row(model = "Random Forest", auc = rf_auc_highest$.estimate)

1.5 De uiteindelijke fit

  • In de laatste stap van deze analyse maken we het model definitief.
  • We testen het model op de testset en evalueren het model met metrieken en de Variable Importance Factor (VIF).

1.5.1 Combineer de AUC/ROC curves en kies het beste model

Eerst combineren we de AUC/ROC curves van de modellen om ze te vergelijken. We kiezen het beste model op basis van de hoogste AUC/ROC.

Toon code
## Combineer de AUC/ROC curves om de modellen te vergelijken
Get_ROC_Plot(list(lr_auc, rf_auc))

Toon code
## Bepaal welke van de modellen het beste is op basis van de hoogste AUC/ROC
dfModel_results <- dfModel_results |>
  mutate(number = row_number()) |> 
  mutate(best = ifelse(auc == max(auc), TRUE, FALSE)) |> 
  arrange(number)

## Bepaal het beste model
sBest_model     <- dfModel_results$model[dfModel_results$best == TRUE]
sBest_model_auc <- round(dfModel_results$auc[dfModel_results$best == TRUE], 4)

Het beste model is het Logistic Regression model met een AUC/ROC van 0.6553. Het Logistic Regression model heeft een AUC van 0.6553. Het Random Forest model heeft een AUC van 0.6427. We ronden de analyse verder af met het Logistic Regression model.

1.5.2 Maak het finale model

We maken het finale model op basis van de beste parameters die we hebben gevonden. Door in de engine bij importance de impurity op te geven, wordt het beste random forest model gekozen om de data definitief mee te classificeren.

## Test het ontwikkelde model op de testset
## Bepaal de optimale parameters

## Bouw de laatste modellen
last_lr_mod <-
    logistic_reg(penalty = lr_best$penalty,
                 mixture = 1) |>
    set_engine("glmnet") |>
    set_mode("classification")

last_rf_mod <-
    rand_forest(mtry = rf_best$mtry,
                min_n = rf_best$min_n,
                trees = 1000) |>
    set_engine("ranger", num.threads = cores, importance = "impurity") |>
    set_mode("classification")

1.5.3 Maak de workflow

We voegen het model toe aan de workflow en updaten de workflow met het finale model.

## Update de workflows
 last_lr_workflow <- 
    lr_workflow |> 
    update_model(last_lr_mod)

 last_rf_workflow <- 
    rf_workflow |> 
    update_model(last_rf_mod)

1.5.4 Fit het finale model

We voeren de finale fit uit. De functie last_fit past het model toe op de validatieset.

## Voer de laatste fit uit
set.seed(2904)

## Maak voor beide modellen een laatste fit, zodat we deze kunnen opslaan voor later gebruik
last_fit_lr <- 
    last_lr_workflow |> 
    last_fit(splits)

last_fit_rf <- 
    last_rf_workflow |> 
    last_fit(splits)

lLast_fits <- list(last_fit_lr, last_fit_rf) |> 
  set_names(c("Logistic Regression", "Random Forest"))

## Bepaal welk model het beste is
if(sBest_model == "Logistic Regression") {
  last_fit <- last_fit_lr
} else if(sBest_model == "Random Forest") {
  last_fit <- last_fit_rf
}

## Bewaar de resultaten, de modelresultaten en de bijbehorende data
sFittedmodels_outputpath <- Get_Model_Outputpath(mode = "last-fits")
saveRDS(lLast_fits, file = sFittedmodels_outputpath)

sModelresults_outputpath <- Get_Model_Outputpath(mode = "modelresults")
saveRDS(dfModel_results, file = sModelresults_outputpath)

sData_outputpath <- Get_Model_Outputpath(mode = "data")
saveRDS(dfOpleiding_inschrijvingen, file = sData_outputpath)

1.5.5 Evalueer het finale model: metrieken en vif

We evalueren het finale model op basis van 4 metrieken: 1) accuraatheid, 2) ROC/AUC en 3) de Brier score (de Mean Squared Error) en 4) de Variable Importance Factor (VIF). Uit de VIF is op te maken welke variabelen het meest bijdragen aan de voorspelling van de uitkomstvariabele.

## Verzamel de metrieken
last_fit |> 
  collect_metrics() |> 
  mutate(.estimate = round(.estimate, 4)) |>
  knitr::kable(col.names = c("Metriek", 
                             "Estimator",
                             "Estimate",
                             "Configuratie"))
Metriek Estimator Estimate Configuratie
accuracy binary 0.6237 Preprocessor1_Model1
roc_auc binary 0.6259 Preprocessor1_Model1
brier_class binary 0.2299 Preprocessor1_Model1
Toon code
# Extraheer de feature importance
dfVif <- last_fit |>
  extract_fit_parsnip() |>
  vip::vi() |> 
  arrange(desc(Importance)) |>
  head(20)
  
# Maak de plot met fill op de variabele 'Importance'
importance_plot <- dfVif |> 
  ggplot(aes(x = reorder(Variable, Importance), 
             y = Importance, 
             fill = Importance)) +
  geom_col(show.legend = FALSE) +
  
  ## Maak de titel en caption
  labs(title = "Meest voorspellende factoren",
       subtitle = "Op basis van de Variable Importance Factor (VIF)",
       x = NULL,
       y = "VIF-score",
       caption = sCaption) +
  
  theme_minimal() +
  Set_LTA_Theme() +
  
  theme(
    axis.title.x = element_text(margin = margin(t = 20))
  ) +
  
  coord_flip()
  
  ## Voeg LTA elementen toe
  importance_plot <- Add_LTA_Theme_Elements(importance_plot, title_subtitle = TRUE)

# Toon de plot
print(importance_plot)

1.5.6 Plot de ROC curve

Tot slot maken we een ROC curve om de prestaties van het definitieve model te visualiseren. De Sensitivity (True Positive Rate) en Specificity (True Negative Rate) worden hierin uitgezet. De Area under the ROC Curve (AUC/ROC) geeft de prestaties van het model weer. Het model scoort beter naarmate de AUC/ROC dichter bij de 1 ligt, de linker bovenhoek. De linker bovenhoek houdt in dat alle prognoses exact overeenstemmen met de werkelijkheid. Een AUC/ROC van 0,5 betekent dat het model niet beter presteert dan een willekeurige voorspelling.

## Toon de roc curve
auc_lf <- last_fit |> 
  collect_predictions() |> 
  roc_curve(Retentie, .pred_FALSE) |> 
  mutate(model = "Last fit")

Get_ROC_Plot(auc_lf, position = 3)

1.6 Conclusies

1.6.1 Het beste prognosemodel voor deze opleiding

Het beste prognosemodel blijkt het Logistic Regression model te zijn.

  • Van de prognosemodellen die we hebben ontwikkeld om retentie na 1 jaar te voorspellen, had het Logistic Regression model de hoogste AUC/ROC waarde (0.6553).

1.6.2 Mate van accuraatheid en lift

Een prognosemodel moet minimaal beter presteren dan een base-model om waarde op accuraatheid toe te voegen. Het base-model neemt de grootste klasse van de gemiddelde retentie na 1 jaar van de afgelopen jaren als basis. Stel we zouden tegen alle studenten zeggen dat ze hun studie gaan halen, dan is de mate van accuratesse gelijk aan dit base-model. Dit base-model is dus altijd hoger dan de 50% lijn van de AUC/ROC curve, tenzij het base-model toevallig precies 50% is.

De mate van accuraatheid van de toepassing van het model is vrij laag (62.37%).

  • Base-model: 60.43% – Voor deze opleiding bereken we het base-model als volgt. Van alle studenten studeerde 60.43% door. De grootste klasse (en de accuratesse) van het base-model is daarmee (100% - 60.43% = ) 60.43% die doorstudeerde.
  • Accuratesse prognose: 62.37% – Het model voorspelt Retentie na 1 jaar met een accuratesse van 62.37%.
  • Lift: 1.94% – Het model scoort in de huidige opbouw met een verschil van 1.94% (de lift) iets beter dan de accuraatheid van het base-model (60.43%).

1.6.3 Confusion Matrix

Toon code
## Bepaal de confusion matrix
confusion_matrix <- last_fit |>
  collect_predictions() |>
  conf_mat(truth = Retentie, estimate = .pred_class) 

dfConf_matrix <- as_tibble(confusion_matrix$table) |>
  rename(Werkelijkheid = Truth) |>
  mutate(Werkelijkheid = ifelse(Werkelijkheid == "TRUE", "Retentie", "Geen retentie"),
         Prediction    = ifelse(Prediction == "TRUE", "Retentie", "Geen retentie"))

pTP <- Change_Number_Marks((dfConf_matrix$n[4]/sum(dfConf_matrix$n)*100),1)
pFP <- Change_Number_Marks((dfConf_matrix$n[2]/sum(dfConf_matrix$n)*100),1)
pTN <- Change_Number_Marks((dfConf_matrix$n[1]/sum(dfConf_matrix$n)*100),1)
pFN <- Change_Number_Marks((dfConf_matrix$n[3]/sum(dfConf_matrix$n)*100),1)
pACC <- Change_Number_Marks(Last_fit_Accuracy,1)

De prestaties van het model kunnen we verder uitdrukken in een confusion matrix. Hierin zien we de voorspellingen van het model en de werkelijke uitkomsten. De matrix geeft inzicht in de mate van correcte en incorrecte voorspellingen. Ter illustratie werken we de matrix uit voor een voorspelling waarop een bindend studieadvies (BSA) gebaseerd zou kunnen zijn.

We passen de confusion matrix nu toe op het model dat als beste naar voren kwam. De accuraatheid van dit model is 62,4%. De accuraatheid van het model berekenen we door de som van de diagonaal te berekenen: het aandeel goed voorspelde uitkomsten, Retentie = Retentie (True Positive) en Geen retentie = Geen retentie (True Negative), af te zetten tegen het totaal aantal voorspellingen: 51,5% + 10,9% = 62,4%. (NB. De weergave in deze confusion matrix is diagonaal gespiegeld vergeleken met het voorbeeld.)

Toon code
confusion_plot <- plot_confusion_matrix(
    dfConf_matrix,
    target_col = "Werkelijkheid",
    prediction_col = "Prediction",
    counts_col = "n",
    palette = "Blues",
    add_sums = TRUE,
    theme_fn = ggplot2::theme_light,
    sums_settings = sum_tile_settings(
      palette = "Greens",
      label = "Totaal",
      tc_tile_border_color = "black"
    )) +
    
    ## Pas de labels aan
    labs(
      title = "Confusion Matrix",
      x = "Werkelijke uitkost",
      y = "Voorspelde uitkomst",
      caption = sCaption
    ) +
    
    Set_LTA_Theme()
  
  ## Voeg LTA elementen toe
  confusion_plot <- Add_LTA_Theme_Elements(confusion_plot, 
                                           title_subtitle = TRUE)
  
  print(confusion_plot)

1.6.4 Uitleggen of verklaren?

Naast de accuraatheid van het model is het ook belangrijk om te weten welke factoren het meest bijdragen aan de voorspelling van retentie na 1 jaar. Daarin gaat de vergelijking met de prestaties van het basemodel mank. Dat model geeft op geen enkele manier aan waarom een student een kans op succes heeft, anders dan - ‘dit is gebruikelijk in deze opleiding’.

Ongeacht de mate van accuraatheid, is het voor ons onderzoek naar kansengelijkheid essentieel om te weten welke factoren het meest bijdragen aan de voorspelling van retentie na 1 jaar. Het gaat erom dat we het belang van de factoren in de voorspellingen kunnen begrijpen en duiden. Machine Learning is hiervoor uitstekend geschikt, omdat het de mogelijkheid biedt om de belangrijkste factoren en hun invloed te leren kennen (Shmueli, 2010; Shmueli & Koppius, 2011).

1.7 Vervolgstappen: Factoranalyse

De volgende stap (stap 2) is een verdiepende analyse van de mate waarin de factoren die we gevonden hebben van invloed zijn op Retentie na 1 jaar. We kijken naar de rangorde, of ze die doorstudeerde verhogen of juist verlagen en hoe stabiel de factoren zijn als we in andere volgordes aan het model toevoegen. Om het concreet te maken zullen we het model toepassen op een aantal fictieve studenten, die we opbouwen uit de meeste voorkomende waarden in deze opleiding. Dit is het onderwerp van analyse 2: de Factoranalyse.

Literatuur

Barocas, S., Hardt, M., & Narayanan, A. (2023). Fairness and Machine Learning: Limitations and Opportunities. fairmlbook.org. http://www.fairmlbook.org
Shmueli, G. (2010). To Explain or to Predict? Statistical Science, 25(3), 289–310. https://doi.org/10.1214/10-sts330
Shmueli, G., & Koppius, O. (2011). Predictive Analytics in Information Systems Research. MIS Quarterly, 35(3), 553. https://doi.org/10.2307/23042796

 

Verantwoording

Deze analyse maakt deel uit van het onderzoek naar kansengelijkheid van het lectoraat Learning Technology & Analytics van De Haagse Hogeschool: No Fairness without Awareness | Het rapport is door het lectoraat ontwikkeld in Quarto 1.4.549. | Template versie: 0.9.1.9000

 

Copyright

Dr. Theo Bakker, Lectoraat Learning Technology & Analytics, De Haagse Hogeschool © 2023-2024. Alle rechten voorbehouden.